using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;

namespace HakoJS.SourceGenerator;

public partial class JSBindingGenerator
{
    #region Enum Model Extraction

    private static EnumResult GetEnumModel(GeneratorAttributeSyntaxContext context, CancellationToken ct)
    {
        if (context.TargetSymbol is not INamedTypeSymbol enumSymbol)
            return new EnumResult(null, ImmutableArray<Diagnostic>.Empty);


        if (enumSymbol.TypeKind != TypeKind.Enum)
            return new EnumResult(null, ImmutableArray.CreateBuilder<Diagnostic>().ToImmutable());

        var jsEnumAttr = enumSymbol.GetAttributes()
            .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "HakoJS.SourceGeneration.JSEnumAttribute");

        var jsEnumName = GetJsEnumName(enumSymbol);
        var casing = NameCasing.None;
        var valueCasing = ValueCasing.Original;

        if (jsEnumAttr != null)
        {
            foreach (var arg in jsEnumAttr.NamedArguments)
            {
                switch (arg)
                {
                    case { Key: "Casing", Value.Value: not null }:
                        casing = (NameCasing)(int)arg.Value.Value;
                        break;
                    case { Key: "ValueCasing", Value.Value: not null }:
                        valueCasing = (ValueCasing)(int)arg.Value.Value;
                        break;
                }
            }
        }

        var isFlags = enumSymbol.GetAttributes()
            .Any(a => a.AttributeClass?.ToDisplayString() == "System.FlagsAttribute");

        var values = new List<EnumValueModel>();
        foreach (var member in enumSymbol.GetMembers().OfType<IFieldSymbol>())
        {
            if (member.IsImplicitlyDeclared || !member.HasConstantValue)
                continue;


            values.Add(new EnumValueModel
            {
                Name = member.Name,
                JsName = member.Name,
                Value = member.ConstantValue ?? 0,
                Documentation = ExtractXmlDocumentation(member),
                NameCasing = casing,
                ValueCasing = valueCasing
            });
        }

        var documentation = ExtractXmlDocumentation(enumSymbol);
        var typeScriptDefinition = GenerateEnumTypeScriptDefinition(
            jsEnumName,
            values,
            isFlags,
            documentation);

        var model = new EnumModel
        {
            EnumName = enumSymbol.Name,
            SourceNamespace = enumSymbol.ContainingNamespace.IsGlobalNamespace
                ? string.Empty
                : enumSymbol.ContainingNamespace.ToDisplayString(),
            JsEnumName = jsEnumName,
            Values = values,
            IsFlags = isFlags,
            TypeScriptDefinition = typeScriptDefinition,
            Documentation = documentation,
            DeclaredAccessibility = enumSymbol.DeclaredAccessibility,
            Symbol = enumSymbol
        };

        return new EnumResult(model, ImmutableArray<Diagnostic>.Empty);
    }

    private static string GetAccessibilityModifier(Accessibility accessibility)
    {
        return accessibility switch
        {
            Accessibility.Public => "public",
            Accessibility.Internal => "internal",
            Accessibility.Private => "private",
            Accessibility.Protected => "protected",
            Accessibility.ProtectedOrInternal => "protected internal",
            Accessibility.ProtectedAndInternal => "private protected",
            _ => "internal"
        };
    }

    #endregion

    #region Enum Binding Generation

    private static string GenerateEnumBinding(
        EnumModel model,
        (Platform Platform, OptimizationLevel OptimizationLevel, string? AssemblyName,
            LanguageVersion? LanguageVersion) compilationSettings)
    {
        var sb = new StringBuilder();

        sb.AppendLine("// <auto-generated/>");
        sb.AppendLine("#nullable enable");
        sb.AppendLine();
        sb.AppendLine("using HakoJS.SourceGeneration;");
        sb.AppendLine();
        
        var isNested = model.Symbol?.ContainingType != null;

        if (!isNested && !string.IsNullOrEmpty(model.SourceNamespace))
            sb.AppendLine($"namespace {model.SourceNamespace};");

        sb.AppendLine();

        var accessibility = GetAccessibilityModifier(model.DeclaredAccessibility);
        
        var fullEnumTypeName = model.Symbol?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
                               ?? $"global::{model.SourceNamespace}.{model.EnumName}";
        
        sb.AppendLine($"{accessibility} static class {model.EnumName}Extensions");
        sb.AppendLine("{");

        sb.AppendLine($"    {accessibility} static string ToStringFast(this {fullEnumTypeName} value)");

        if (model.IsFlags)
        {
            sb.AppendLine("    {");
            sb.AppendLine("        return value switch");
            sb.AppendLine("        {");
            
            var zeroValue = model.Values.FirstOrDefault(v => Convert.ToInt64(v.Value) == 0);
            sb.AppendLine(zeroValue != null
                ? $"            0 => nameof({fullEnumTypeName}.{zeroValue.Name}),"
                : "            0 => \"0\",");
            
            foreach (var enumValue in model.Values.Where(v => Convert.ToInt64(v.Value) != 0))
            {
                sb.AppendLine(
                    $"            {fullEnumTypeName}.{enumValue.Name} => nameof({fullEnumTypeName}.{enumValue.Name}),");
            }

            sb.AppendLine("            _ => FormatFlags(value)");
            sb.AppendLine("        };");
            sb.AppendLine();
            
            sb.AppendLine($"        static string FormatFlags({fullEnumTypeName} value)");
            sb.AppendLine("        {");
            sb.AppendLine("            var flags = new global::System.Collections.Generic.List<string>();");
            sb.AppendLine();

            foreach (var enumValue in model.Values.Where(v => Convert.ToInt64(v.Value) != 0))
            {
                sb.AppendLine(
                    $"            if ((value & {fullEnumTypeName}.{enumValue.Name}) == {fullEnumTypeName}.{enumValue.Name})");
                sb.AppendLine($"                flags.Add(nameof({fullEnumTypeName}.{enumValue.Name}));");
            }

            sb.AppendLine();
            sb.AppendLine(
                "            return flags.Count > 0 ? global::System.String.Join(\", \", flags) : value.ToString();");
            sb.AppendLine("        }");
            sb.AppendLine("    }");
        }
        else
        {
            sb.AppendLine("        => value switch");
            sb.AppendLine("        {");

            foreach (var enumValue in model.Values)
            {
                sb.AppendLine(
                    $"            {fullEnumTypeName}.{enumValue.Name} => nameof({fullEnumTypeName}.{enumValue.Name}),");
            }

            sb.AppendLine("            _ => value.ToString(),");
            sb.AppendLine("        };");
        }

        sb.AppendLine("}");
        sb.AppendLine();

        var useCSharp14Extensions = compilationSettings.LanguageVersion is >= LanguageVersion.CSharp14;

        if (useCSharp14Extensions)
        {
            // Use C# 14+ extension syntax
            sb.AppendLine($"{accessibility} static class {model.EnumName}TypeDefinition");
            sb.AppendLine("{");
            sb.AppendLine($"    extension({fullEnumTypeName})");
            sb.AppendLine("    {");
            sb.AppendLine($"        {accessibility} static string TypeDefinition");
            sb.AppendLine("        {");
            sb.AppendLine("            get");
            sb.AppendLine("            {");

            var escapedTypeScript = model.TypeScriptDefinition.Replace("\"", "\"\"");
            sb.AppendLine("                return @\"");
            sb.Append(escapedTypeScript);
            sb.AppendLine("\";");

            sb.AppendLine("            }");
            sb.AppendLine("        }");
            sb.AppendLine("    }");
            sb.AppendLine("}");
        }
        else
        {
            sb.AppendLine($"{accessibility} static class {model.EnumName}TypeDefinition");
            sb.AppendLine("{");
            sb.AppendLine($"    {accessibility} static string GetTypeDefinition()");
            sb.AppendLine("    {");

            var escapedTypeScript = model.TypeScriptDefinition.Replace("\"", "\"\"");
            sb.AppendLine("        return @\"");
            sb.Append(escapedTypeScript);
            sb.AppendLine("\";");

            sb.AppendLine("    }");
            sb.AppendLine("}");
        }

        return sb.ToString();
    }

    private static void GenerateEnumSource(
        SourceProductionContext context,
        EnumModel model,
        (Platform Platform, OptimizationLevel OptimizationLevel, string? AssemblyName, LanguageVersion? LanguageVersion)
            compilationSettings)
    {
        var source = GenerateEnumBinding(model, compilationSettings);
        context.AddSource($"{model.EnumName}.Enum.g.cs", SourceText.From(source, Encoding.UTF8));
    }

    #endregion
}